package com.jappyframework;

import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.jappyframework.JavaSource.AccessSpecifier;
import com.jappyframework.api.DefaultMethod;
import com.jappyframework.api.GetInstance;
import com.jappyframework.api.GetObject;
import com.jappyframework.api.HTTPRequestType;
import com.jappyframework.api.WebMethod;
import com.sun.mirror.apt.AnnotationProcessorEnvironment;
import com.sun.mirror.declaration.ClassDeclaration;
import com.sun.mirror.declaration.MethodDeclaration;
import com.sun.mirror.declaration.Modifier;
import com.sun.mirror.declaration.ParameterDeclaration;
import com.sun.mirror.type.ArrayType;
import com.sun.mirror.type.DeclaredType;
import com.sun.mirror.type.PrimitiveType;
import com.sun.mirror.type.PrimitiveType.Kind;

/**
 * Visitor to process recipe/potion annotations.
 * 
 * FIXME :- If response type is String the default responseType can be TEXT. No
 * need to mention it. Infer ResponseType from return type. TODO :- Support
 * polymorphism. Variable arguments should be allowed.
 * 
 * @author harsh
 */
public class WebServiceGenerator {

	private static final String METHOD_PREFIX = "iDo";

	/**
	 * 
	 * @param classDecl
	 * @param env
	 */
	public WebServiceGenerator(ClassDeclaration classDecl,
			AnnotationProcessorEnvironment env) {
		this.mCDecl = classDecl;
		this.mGetInstanceDecl = getInstanceMethodDecl();
		this.mEnv = env;
		this.mSource = new JavaSource(" ");
		this.mPotionsByType = new HashMap<HTTPRequestType, Set<MethodDeclaration>>();
		for (HTTPRequestType type : HTTPRequestType.values()) {
			mPotionsByType.put(type, new HashSet<MethodDeclaration>());
		}
		this.mDefaultPotions = new HashMap<HTTPRequestType, MethodDeclaration>();
		this.mHasGetInstance = false;
	}

	/**
	 * Add Initial imports
	 */
	private void addInitialImports() {
		mSource.printImport("javax.servlet.http.HttpServlet");
		mSource.printImport("javax.servlet.http.HttpServletRequest");
		mSource.printImport("javax.servlet.http.HttpServletResponse");
		mSource.printImport("java.io.IOException");
		mSource.printImport("javax.servlet.ServletException");
		mSource.printImport("org.apache.log4j.Logger");

		mSource.printImport(mCDecl.getQualifiedName());
	}

	/**
	 * Check wheteher this class has a proper getInstance.
	 */
	private void checkGetInstance() {
		mHasGetInstance = false;
		// If nobody has been annotated as getInstance.
		if (mGetInstanceDecl == null) {
			return;
		}

		// A Get Instance method must be public static.
		Collection<Modifier> modifiers = mGetInstanceDecl.getModifiers();
		if (!modifiers.contains(Modifier.PUBLIC)) {
			return;
		}
		if (!modifiers.contains(Modifier.STATIC)) {
			return;
		}

		// Last thing is to verify return type.
		if (!(mGetInstanceDecl.getReturnType() instanceof DeclaredType)) {
			return;
		}

		DeclaredType returnType = (DeclaredType) mGetInstanceDecl
				.getReturnType();
		if (!returnType.getDeclaration().equals(mCDecl)) {
			return;
		}
		mHasGetInstance = true;
	}

	/**
	 * Generate parsing code from HTTPRequest for the following parameters. <br>
	 * If declaration is (int a, int b, String c). The code generated is <br>
	 * int a = Integer.parseInt(a); etc. <br>
	 * Duplicates are allowed. We should check that no two parameters with same
	 * simple name have different types.
	 * 
	 * @param parameters
	 */
	private boolean generateParsingCode(List<ParameterDeclaration> parameters) {
		Map<String, ParameterDeclaration> m = new HashMap<String, ParameterDeclaration>();
		for (ParameterDeclaration para : parameters) {
			ParameterDeclaration mp = m.get(para.getSimpleName());
			if (mp == null) {
				m.put(para.getSimpleName(), para);
			} else if (!mp.getType().equals(para.getType())) {
				return false;
			}
		}

		for (Map.Entry<String, ParameterDeclaration> entry : m.entrySet()) {
			generateParsingCode(entry.getValue());
		}
		for (Map.Entry<String, ParameterDeclaration> entry : m.entrySet()) {
			generateParsingCodeForObjects(entry.getValue());
		}

		return true;
	}

	/**
	 * @param pd
	 * @return
	 */
	private boolean isProducerPresent(ParameterDeclaration pd) {
		return getProducer(pd) != null;
	}

	/**
	 * @param pd
	 * @return
	 */
	private MethodDeclaration getProducer(ParameterDeclaration pd) {
		for (MethodDeclaration md : mCDecl.getMethods()) {
			if (md.getAnnotation(GetObject.class) == null) {
				continue;
			}
			if (md.getReturnType().equals(pd.getType())) {
				return md;
			}
		}
		return null;
	}

	private void generateParsingCodeForObjects(ParameterDeclaration para) {
		if (isProducerPresent(para)) {
			MethodDeclaration md = getProducer(para);
			DeclaredType dt = (DeclaredType) md.getReturnType();
			mSource.printImport(dt.getDeclaration().getQualifiedName());

			String methodCall = getInstance(md) + "." + md.getSimpleName()
					+ "(" + getMethodArgs(md) + ")";
			mSource.printText(dt.getDeclaration().getSimpleName() + " "
					+ para.getSimpleName() + " = " + methodCall + ";");

		}
	}

	/**
	 * Generate parsing code for a parameter. Allowed types are
	 * primitives/String and there arrays.
	 * 
	 * @param para
	 */
	private void generateParsingCode(ParameterDeclaration para) {
		if (TypeUtils.isTypeArray(para.getType())) {
			ArrayType array = (ArrayType) para.getType();
			String arraySuffix = "Array";
			if (TypeUtils.isTypeString(array.getComponentType())) {
				arraySuffix = "";

				mSource.printText("String[] " + para.getSimpleName()
						+ arraySuffix + " = request.getParameterValues(\""
						+ para.getSimpleName() + "\");");
			} else if (TypeUtils.isTypePrimitive(array.getComponentType())) {
				PrimitiveType paraType = (PrimitiveType) array
						.getComponentType();
				mSource
						.printText(paraType.getKind().name().toLowerCase()
								+ "[] " + para.getSimpleName() + " = new "
								+ paraType.getKind().name().toLowerCase() + "["
								+ para.getSimpleName() + arraySuffix
								+ ".length" + "];");
				mSource.printText("for (int pi = 0; pi < "
						+ para.getSimpleName() + arraySuffix
						+ ".length; ++pi) {");
				{
					mSource.increaseIndent();
					mSource.printText(para.getSimpleName() + "[pi] = "
							+ getParsingCommand(paraType.getKind()) + "("
							+ para.getSimpleName() + arraySuffix + "[pi]"
							+ ");");
					mSource.decreaseIndent();
				}
				mSource.closeElement();
			} else {
				mEnv.getMessager().printError(para.getPosition(),
						"Only String and primitives allowed in array");
			}
		} else if (TypeUtils.isTypePrimitive(para.getType())) {
			PrimitiveType paraType = (PrimitiveType) para.getType();
			mSource.printText(paraType.getKind().name().toLowerCase() + " "
					+ para.getSimpleName() + " = "
					+ getParsingCommand(paraType.getKind())
					+ "(request.getParameter(\"" + para.getSimpleName()
					+ "\"));");
		} else if (TypeUtils.isTypeString(para.getType())) {
			mSource.printText("String " + para.getSimpleName()
					+ " = request.getParameter(\"" + para.getSimpleName()
					+ "\");");
		} else if (TypeUtils.isTypeChildOf(para.getType(), HttpSession.class)) {
			mSource.printImport("javax.servlet.http.HttpSession");
			mSource.printText("HttpSession " + para.getSimpleName()
					+ " = request.getSession();");
		} else if (TypeUtils.isTypeChildOf(para.getType(),
				HttpServletRequest.class)) {
			// Dont do anything.. request is already in scope.
		} else if (TypeUtils.isTypeChildOf(para.getType(),
				HttpServletResponse.class)) {
			// Dont do anytihng.
		} else if (TypeUtils.isTypeChildOf(para.getType(), Map.class)) {
			mSource.printImport("java.util.Map");
			mSource.printText("Map " + para.getSimpleName()
					+ " = request.getParameterMap();");
		} else if (isProducerPresent(para)) {
		} else {
			mEnv.getMessager().printError(para.getPosition(),
					"Illegal parameter type");
		}
	}

	private void generateRequestMethod(HTTPRequestType type) {
		// Skip if no potion of the type exists.
		if (mPotionsByType.get(type).size() == 0) {
			return;
		}
		mSource.printText("@Override");
		mSource.openMethod(false, //															 
				AccessSpecifier.PROTECTED, "void", //										
				"do" + StringUtils.capitalizeFirstCharacter(type.name()), // 
				new String[] { "HttpServletRequest", "HttpServletResponse" }, // 
				new String[] { "request", "response" }, //
				new String[] { "IOException", "ServletException" }, // 
				true);
		mSource.increaseIndent();
		{
			mSource.printText("LOG.debug(request.getQueryString());");
			mSource.printText( //
					"String methodName = request.getParameter(\"m\");");
			mSource.printText("if (methodName == null) {");
			mSource.increaseIndent();
			{
				MethodDeclaration defaultMethod = mDefaultPotions.get(type);
				if (defaultMethod == null) {
					mSource
							.printText("response.sendError(402, \"Method Name is empty\");");
				} else {
					mSource.printText(METHOD_PREFIX
							+ StringUtils
									.capitalizeFirstCharacter(defaultMethod
											.getSimpleName())
							+ "(request, response);");
				}
				mSource.printText("return;");
				mSource.closeElement();
			}

			for (MethodDeclaration methodDecl : mPotionsByType.get(type)) {
				mSource.printText( //
						"if (methodName.equals(\"" + methodDecl.getSimpleName()
								+ "\")) {");
				mSource.increaseIndent();
				{
					mSource.printText(METHOD_PREFIX
							+ StringUtils.capitalizeFirstCharacter(methodDecl
									.getSimpleName()) + "(request, response);");
					mSource.printText("return;");
					mSource.closeElement();
				}
			} // for
			mSource
					.printText("response.sendError(400, \"No matching method found\");");
			mSource.closeElement();
		}

	}

	/**
	 * Returns the access to the instance. If the method is static just return
	 * classDecl else return getInstance method.
	 * 
	 * @param methodDecl
	 * @return
	 */
	private String getInstance(MethodDeclaration methodDecl) {
		if (methodDecl.getModifiers().contains(Modifier.STATIC)) {
			return mCDecl.getSimpleName();
		}
		if (!mHasGetInstance) {
			mEnv.getMessager().printError(methodDecl.getPosition(),
					"No get instance method");
			return "";
		}

		return mCDecl.getSimpleName() + "." + mGetInstanceDecl.getSimpleName()
				+ "(" + getMethodArgs(mGetInstanceDecl) + ")";
	}

	/**
	 * 
	 * @return
	 */
	private MethodDeclaration getInstanceMethodDecl() {
		for (MethodDeclaration method : mCDecl.getMethods()) {
			if (method.getAnnotation(GetInstance.class) != null) {
				return method;
			}
		}
		return null;
	}

	private String getMethodArgs(MethodDeclaration method) {
		StringBuffer sb = new StringBuffer();
		for (ParameterDeclaration paraDecl : method.getParameters()) {
			sb.append(paraDecl.getSimpleName());
			sb.append(",");
		}
		// Remove last spurious ","
		if (sb.length() >= 1) {
			sb.deleteCharAt(sb.length() - 1);
		}
		return sb.toString();
	}

	private String getParsingCommand(Kind kind) {
		switch (kind) {
		case BOOLEAN:
			return "Boolean.parseBoolean";
		case BYTE:
			return "Byte.parseByte";
		case CHAR:
			return "Char.parseChar";
		case DOUBLE:
			return "Double.parseDouble";
		case FLOAT:
			return "Float.parseFloat";
		case INT:
			return "Integer.parseInt";
		case LONG:
			return "Long.parseLong";
		case SHORT:
			return "Short.parseShort";
		}
		return "";
	}

	/**
	 * Return the generated Java source code.
	 * 
	 * @return
	 */
	public JavaSource getSource() {
		return mSource;
	}

	private String getToStringCommand(Kind kind) {
		switch (kind) {
		case BOOLEAN:
			return "Boolean.toString";
		case BYTE:
			return "Byte.toString";
		case CHAR:
			return "Char.toString";
		case DOUBLE:
			return "Double.toString";
		case FLOAT:
			return "Float.toString";
		case INT:
			return "Integer.toString";
		case LONG:
			return "Long.toString";
		case SHORT:
			return "Short.toString";
		}
		return "";
	}

	/**
	 * A method is a proper potion if all its parameters are primitives and also
	 * the return type is primitive.
	 * 
	 * @param methodDecl
	 * @return
	 */
	private boolean isProperPotion(MethodDeclaration methodDecl) {
		// Parameters should be primitive
		for (ParameterDeclaration para : methodDecl.getParameters()) {
			if (!isProperPotionArgument(para)) {
				mEnv.getMessager().printWarning(para.getPosition(),
						"This is not a proper argument");
				return false;
			}
		}

		// Non-static potions need to have getInstance present.
		boolean hasStatic = methodDecl.getModifiers().contains(Modifier.STATIC);
		if (!hasStatic && !mHasGetInstance) {
			return false;
		}
		return true;
	}

	/**
	 * Only allow primitives and arrays of primitives to go through. Ofcourse
	 * allow String.
	 * 
	 * @param parameter
	 * @return
	 */
	private boolean isProperPotionArgument(ParameterDeclaration parameter) {
		if (TypeUtils.isTypePrimitive(parameter.getType())) {
			return true;
		}
		if (TypeUtils.isTypeString(parameter.getType())) {
			return true;
		}
		if (TypeUtils.isTypeChildOf(parameter.getType(), HttpSession.class)) {
			return true;
		}
		if (TypeUtils.isTypeChildOf(parameter.getType(),
				HttpServletRequest.class)) {
			return true;
		}
		if (TypeUtils.isTypeChildOf(parameter.getType(),
				HttpServletResponse.class)) {
			return true;
		}
		if (TypeUtils.isTypeChildOf(parameter.getType(), Map.class)) {
			return true;
		}
		if (TypeUtils.isTypeChildOf(parameter.getType(),
				HttpServletRequest.class)) {
			return true;
		}
		if (TypeUtils.isTypeChildOf(parameter.getType(),
				HttpServletResponse.class)) {
			return true;
		}
		if (TypeUtils.isTypeArray(parameter.getType())) {
			ArrayType array = (ArrayType) parameter.getType();
			if (TypeUtils.isTypePrimitive(array.getComponentType()))
				return true;
			if (TypeUtils.isTypeString(array.getComponentType()))
				return true;
		}
		if (isProducerPresent(parameter)) {
			return true;
		}
		return false;
	}

	/**
	 * Process the class.
	 * 
	 * @param mCDecl
	 * @throws FileNotFoundException 
	 */
	public void processClass() {
		Logger.log(mCDecl);
		checkGetInstance();
		mSource.printPackage(mCDecl.getPackage().getQualifiedName());
		addInitialImports();

		String genClassName = mCDecl.getSimpleName() + "Servlet";
		mSource.openClass(false, AccessSpecifier.PUBLIC, genClassName,
				"HttpServlet", null);
		mSource
				.printText("private static final Logger LOG = Logger.getLogger(\""
						+ mCDecl.getQualifiedName() + "\");");
		for (MethodDeclaration methodDecl : mCDecl.getMethods()) {
			processMethod(methodDecl);
		}
		for (HTTPRequestType type : HTTPRequestType.values()) {
			generateRequestMethod(type);
		}
		mSource.closeElement();
	}

	/**
	 * Every method may need first other methods to invoke. GetInstance and
	 * GetObject for earch object it creates.
	 * 
	 * @param md
	 * @return
	 */
	private List<MethodDeclaration> methodsToInvoke(MethodDeclaration md) {
		ArrayList<MethodDeclaration> methods = new ArrayList<MethodDeclaration>();
		if (mHasGetInstance && !md.getModifiers().contains(Modifier.STATIC)) {
			methods.add(getInstanceMethodDecl());
		}
		for (ParameterDeclaration pd : md.getParameters()) {
			if (isProducerPresent(pd)) {
				methods.add(getProducer(pd));
			}
		}
		return methods;
	}

	private void processMethod(MethodDeclaration methodDecl) {
		WebMethod potion = methodDecl.getAnnotation(WebMethod.class);
		if (potion == null)
			return;

		if (!isProperPotion(methodDecl)) {
			mEnv.getMessager().printError(methodDecl.getPosition(),
					"Not a proper potion");
			return;
		}

		// Add this method to all the request types we belong to.
		for (HTTPRequestType type : potion.requestType()) {
			mPotionsByType.get(type).add(methodDecl);
		}

		// If this is the default method we add it to.
		if (methodDecl.getAnnotation(DefaultMethod.class) != null) {
			for (HTTPRequestType type : potion.requestType()) {
				mDefaultPotions.put(type, methodDecl);
			}
		}

		mSource.newLine();
		mSource.openMethod(false, //
				AccessSpecifier.PRIVATE, // 
				"void", METHOD_PREFIX
						+ StringUtils.capitalizeFirstCharacter(methodDecl //
								.getSimpleName()), // 
				new String[] { "HttpServletRequest", "HttpServletResponse" }, //
				new String[] { "request", "response" }, //
				new String[] { "IOException" }, // 
				true);
		mSource.increaseIndent();
		{
			mSource.printText("try {");
			{
				mSource.increaseIndent();

				// Construct a list of parameter declaration to parse.
				// They come from 3 places as we speak
				// 1) From the method itself.
				// 2) If Getinstance call needs any.
				// 3) If Verify method needs any.

				List<ParameterDeclaration> parameters = new ArrayList<ParameterDeclaration>();
				List<MethodDeclaration> methodsToInvoke = methodsToInvoke(methodDecl);
				
				for (MethodDeclaration mti : methodsToInvoke) {
					parameters.addAll(mti.getParameters());
				}
				parameters.addAll(methodDecl.getParameters());

				if (!generateParsingCode(parameters)) {
					mEnv.getMessager().printError(mCDecl.getPosition(),
							"Duplicate parameters in getInstance and a potion");
				}

				if (!TypeUtils.isTypeVoid(methodDecl.getReturnType())){
					mSource.printText("String retValue = ");
				}

				String methodCall = getInstance(methodDecl) + "."
						+ methodDecl.getSimpleName() + "("
						+ getMethodArgs(methodDecl) + ")";

				// If a method call returns String we may need to create string
				// out of the returning type.
				if (!TypeUtils.isTypeVoid(methodDecl.getReturnType())) {
					if (TypeUtils.isTypePrimitive(methodDecl.getReturnType())) {
						PrimitiveType primitiveRet = (PrimitiveType) methodDecl
								.getReturnType();
						methodCall = getToStringCommand(primitiveRet.getKind())
								+ "(" + methodCall + ")";
					} else if (!TypeUtils.isTypeString(methodDecl
							.getReturnType())) {
						methodCall += ".toString()";
					}
				}
				mSource.printText(methodCall + ";");

				if (!TypeUtils.isTypeVoid(methodDecl.getReturnType())) {
					mSource.printText("response.getWriter().write(retValue);");
					mSource.printText("response.getWriter().flush();");
				}
				mSource.closeElement();
			}
			mSource.printText("catch (Exception e) {");
			{
				mSource.increaseIndent();
				mSource.printText("LOG.error(\"Exception in "
						+ methodDecl.getSimpleName() + "\", e);");
				mSource.printText("response.sendError(400, e.getMessage());");
				mSource.closeElement();
			}
			mSource.closeElement();
		}
		mSource.newLine();
	}

	/**
	 * Class declaration for this recipe.
	 */
	private final ClassDeclaration mCDecl;

	private Map<HTTPRequestType, MethodDeclaration> mDefaultPotions;

	/**
	 * ENV passed to us by factory.
	 */
	private AnnotationProcessorEnvironment mEnv;

	/**
	 * Method declaration for the getInstance
	 */
	private final MethodDeclaration mGetInstanceDecl;

	/**
	 * Variable to track whether this class defines a get instance method. If it
	 * does than non-static potions are allowed, else they are not allowed.
	 */
	private boolean mHasGetInstance;

	/**
	 * Local index of all potions by their request types. One potion can occur
	 * multipe types.
	 */
	private Map<HTTPRequestType, Set<MethodDeclaration>> mPotionsByType;

	/**
	 * Source in memory while we generate it.
	 */
	private JavaSource mSource;

}
